/**
* Copyright 2013, Big Switch Networks, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
**/
package net.floodlightcontroller.restserver;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.restlet.Application;
import org.restlet.Component;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.Restlet;
import org.restlet.Server;
import org.restlet.data.Header;
import org.restlet.data.Parameter;
import org.restlet.data.Protocol;
import org.restlet.data.Reference;
import org.restlet.data.Status;
import org.restlet.engine.header.HeaderConstants;
import org.restlet.ext.jackson.JacksonRepresentation;
import org.restlet.representation.Representation;
import org.restlet.routing.Filter;
import org.restlet.routing.Router;
import org.restlet.routing.Template;
import org.restlet.service.StatusService;
import org.restlet.util.Series;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.floodlightcontroller.core.internal.FloodlightProvider;
import net.floodlightcontroller.core.module.FloodlightModuleContext;
import net.floodlightcontroller.core.module.FloodlightModuleException;
import net.floodlightcontroller.core.module.IFloodlightModule;
import net.floodlightcontroller.core.module.IFloodlightService;
public class RestApiServer implements IFloodlightModule, IRestApiService {
protected static Logger logger = LoggerFactory.getLogger(RestApiServer.class);
protected List<RestletRoutable> restlets;
protected FloodlightModuleContext fmlContext;
protected String restHost = null;
private static String keyStorePassword;
private static String keyStore;
private static String httpsNeedClientAuth = "true";
private static boolean accessControlAllowAllOrigins = false;
private static boolean useHttps = false;
private static boolean useHttp = false;
private static String httpsPort;
private static String httpPort;
// ***********
// Application
// ***********
protected class RestApplication extends Application {
protected Context context;
public RestApplication() {
super(new Context());
this.context = getContext();
}
@Override
public Restlet createInboundRoot() {
Router baseRouter = new Router(context);
baseRouter.setDefaultMatchingMode(Template.MODE_STARTS_WITH);
for (RestletRoutable rr : restlets) {
baseRouter.attach(rr.basePath(), rr.getRestlet(context));
}
Filter slashFilter = new Filter() {
@Override
protected int beforeHandle(Request request, Response response) {
Reference ref = request.getResourceRef();
String originalPath = ref.getPath();
if (originalPath.contains("//"))
{
String newPath = originalPath.replaceAll("/+", "/");
ref.setPath(newPath);
}
return Filter.CONTINUE;
}
};
if (accessControlAllowAllOrigins) {
Filter crossAccessAllowAll = new Filter() {
@Override
protected int beforeHandle(Request request, Response response) {
// Initialize response headers
@SuppressWarnings("unchecked")
Series<Header> responseHeaders = (Series<Header>) response
.getAttributes().get(HeaderConstants.ATTRIBUTE_HEADERS);
if (responseHeaders == null) {
responseHeaders = new Series<Header>(Header.class);
}
// Request headers
@SuppressWarnings("unchecked")
Series<Header> requestHeaders = (Series<Header>) request
.getAttributes().get(HeaderConstants.ATTRIBUTE_HEADERS);
String requestOrigin = requestHeaders.getFirstValue("Origin",
false, "*");
String rh = requestHeaders.getFirstValue(
"Access-Control-Request-Headers", false, "*");
// Set CORS headers in response
responseHeaders.set(
"Access-Control-Expose-Headers",
"Authorization, Link");
responseHeaders.set("Access-Control-Allow-Credentials", "true");
responseHeaders.set("Access-Control-Allow-Methods",
"GET,POST,PUT,DELETE");
responseHeaders.set("Access-Control-Allow-Origin", requestOrigin);
responseHeaders.set("Access-Control-Allow-Headers", rh);
// Set response headers
response.getAttributes().put(HeaderConstants.ATTRIBUTE_HEADERS,
responseHeaders);
// Handle HTTP methods
if (org.restlet.data.Method.OPTIONS.equals(request.getMethod())) {
return Filter.STOP;
}
return Filter.CONTINUE;
}
};
crossAccessAllowAll.setNext(slashFilter);
slashFilter.setNext(baseRouter);
return crossAccessAllowAll; /* caaa --> sf --> br */
}
slashFilter.setNext(baseRouter);
return slashFilter; /* sf --> br */
}
public void run(FloodlightModuleContext fmlContext, String restHost) {
setStatusService(new StatusService() {
@Override
public Representation getRepresentation(Status status,
Request request,
Response response) {
return new JacksonRepresentation<Status>(status);
}
});
// Add everything in the module context to the rest
for (Class<? extends IFloodlightService> s : fmlContext.getAllServices()) {
if (logger.isTraceEnabled()) {
logger.trace("Adding {} for service {} into context",
s.getCanonicalName(), fmlContext.getServiceImpl(s));
}
context.getAttributes().put(s.getCanonicalName(),
fmlContext.getServiceImpl(s));
}
/*
* Specifically add the FML for use by the REST API's /wm/core/modules/...
*/
context.getAttributes().put(fmlContext.getModuleLoader().getClass().getCanonicalName(), fmlContext.getModuleLoader());
/* Start listening for REST requests */
try {
final Component component = new Component();
if (RestApiServer.useHttps) {
Server server;
if (restHost == null) {
server = component.getServers().add(Protocol.HTTPS, Integer.valueOf(RestApiServer.httpsPort));
} else {
server = component.getServers().add(Protocol.HTTPS, restHost, Integer.valueOf(RestApiServer.httpsPort));
}
Series<Parameter> parameters = server.getContext().getParameters();
//parameters.add("sslContextFactory", "org.restlet.ext.jsslutils.PkixSslContextFactory");
parameters.add("sslContextFactory", "org.restlet.engine.ssl.DefaultSslContextFactory");
parameters.add("keystorePath", RestApiServer.keyStore);
parameters.add("keystorePassword", RestApiServer.keyStorePassword);
parameters.add("keyPassword", RestApiServer.keyStorePassword);
parameters.add("keystoreType", "JKS");
parameters.add("truststorePath", RestApiServer.keyStore);
parameters.add("truststorePassword", RestApiServer.keyStorePassword);
parameters.add("trustPassword", RestApiServer.keyStorePassword);
parameters.add("truststoreType", "JKS");
parameters.add("needClientAuthentication", RestApiServer.httpsNeedClientAuth);
}
if (RestApiServer.useHttp) {
if (restHost == null) {
component.getServers().add(Protocol.HTTP, Integer.valueOf(RestApiServer.httpPort));
} else {
component.getServers().add(Protocol.HTTP, restHost, Integer.valueOf(RestApiServer.httpPort));
}
}
component.getClients().add(Protocol.CLAP);
component.getDefaultHost().attach(this);
component.start();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
// ***************
// IRestApiService
// ***************
@Override
public void addRestletRoutable(RestletRoutable routable) {
restlets.add(routable);
}
@Override
public void run() {
if (logger.isDebugEnabled()) {
StringBuffer sb = new StringBuffer();
sb.append("REST API routables: ");
for (RestletRoutable routable : restlets) {
sb.append(routable.getClass().getSimpleName());
sb.append(" (");
sb.append(routable.basePath());
sb.append("), ");
}
logger.debug(sb.toString());
}
RestApplication restApp = new RestApplication();
restApp.run(fmlContext, restHost);
}
// *****************
// IFloodlightModule
// *****************
@Override
public Collection<Class<? extends IFloodlightService>> getModuleServices() {
Collection<Class<? extends IFloodlightService>> services =
new ArrayList<Class<? extends IFloodlightService>>(1);
services.add(IRestApiService.class);
return services;
}
@Override
public Map<Class<? extends IFloodlightService>, IFloodlightService>
getServiceImpls() {
Map<Class<? extends IFloodlightService>,
IFloodlightService> m =
new HashMap<Class<? extends IFloodlightService>,
IFloodlightService>();
m.put(IRestApiService.class, this);
return m;
}
@Override
public Collection<Class<? extends IFloodlightService>> getModuleDependencies() {
// We don't have any
return null;
}
@Override
public void init(FloodlightModuleContext context)
throws FloodlightModuleException {
// This has to be done here since we don't know what order the
// startUp methods will be called
this.restlets = new ArrayList<RestletRoutable>();
this.fmlContext = context;
// read our config options
Map<String, String> configOptions = context.getConfigParams(this);
restHost = configOptions.get("host");
if (restHost == null) {
Map<String, String> providerConfigOptions = context.getConfigParams(
FloodlightProvider.class);
restHost = providerConfigOptions.get("openflowhost");
}
if (restHost != null) {
logger.debug("REST host set to {}", restHost);
}
String path = configOptions.get("keyStorePath");
String pass = configOptions.get("keyStorePassword");
String useHttps = configOptions.get("useHttps");
String useHttp = configOptions.get("useHttp");
String httpsNeedClientAuth = configOptions.get("httpsNeedClientAuthentication");
String accessControlAllowOrigin = configOptions.get("accessControlAllowAllOrigins");
/* HTTPS Access (ciphertext) */
if (useHttps == null || path == null || path.isEmpty() ||
(!useHttps.trim().equalsIgnoreCase("yes") && !useHttps.trim().equalsIgnoreCase("true") &&
!useHttps.trim().equalsIgnoreCase("yep") && !useHttps.trim().equalsIgnoreCase("ja") &&
!useHttps.trim().equalsIgnoreCase("stimmt")
)
) {
RestApiServer.useHttps = false;
RestApiServer.keyStore = null;
RestApiServer.keyStorePassword = null;
} else {
RestApiServer.useHttps = true;
RestApiServer.keyStore = path;
RestApiServer.keyStorePassword = (pass == null ? "" : pass);
String port = configOptions.get("httpsPort");
if (port != null && !port.isEmpty()) {
RestApiServer.httpsPort = port.trim();
}
if (httpsNeedClientAuth == null || (!httpsNeedClientAuth.trim().equalsIgnoreCase("yes") &&
!httpsNeedClientAuth.trim().equalsIgnoreCase("true") &&
!httpsNeedClientAuth.trim().equalsIgnoreCase("yep") &&
!httpsNeedClientAuth.trim().equalsIgnoreCase("ja") &&
!httpsNeedClientAuth.trim().equalsIgnoreCase("stimmt"))
) {
RestApiServer.httpsNeedClientAuth = "false";
} else {
RestApiServer.httpsNeedClientAuth = "true";
}
}
/* HTTP Access (plaintext) */
if (useHttp == null ||
(!useHttp.trim().equalsIgnoreCase("yes") && !useHttp.trim().equalsIgnoreCase("true") &&
!useHttp.trim().equalsIgnoreCase("yep") && !useHttp.trim().equalsIgnoreCase("ja") &&
!useHttp.trim().equalsIgnoreCase("stimmt")
)
) {
RestApiServer.useHttp = false;
} else {
RestApiServer.useHttp = true;
String port = configOptions.get("httpPort");
if (port != null && !port.isEmpty()) {
RestApiServer.httpPort = port.trim();
}
}
if (RestApiServer.useHttp && RestApiServer.useHttps && RestApiServer.httpPort.equals(RestApiServer.httpsPort)) {
logger.error("REST API's HTTP and HTTPS ports cannot be the same. Got " + RestApiServer.httpPort + " for both.");
throw new IllegalArgumentException("REST API's HTTP and HTTPS ports cannot be the same. Got " + RestApiServer.httpPort + " for both.");
}
if (!RestApiServer.useHttps) {
logger.warn("HTTPS disabled; HTTPS will not be used to connect to the REST API.");
} else {
if (RestApiServer.httpsNeedClientAuth.equals("true")) {
logger.warn("HTTPS enabled; Only trusted clients permitted. Allowing secure access to REST API on port {}.", RestApiServer.httpsPort);
} else {
logger.warn("HTTPS enabled; All clients permitted. Allowing secure access to REST API on port {}.", RestApiServer.httpsPort);
}
logger.info("HTTPS' SSL keystore/truststore path: {}, password: {}", RestApiServer.keyStore, RestApiServer.keyStorePassword);
}
if (!RestApiServer.useHttp) {
logger.warn("HTTP disabled; HTTP will not be used to connect to the REST API.");
} else {
logger.warn("HTTP enabled; Allowing unsecure access to REST API on port {}.", RestApiServer.httpPort);
}
if (accessControlAllowOrigin != null) {
try {
RestApiServer.accessControlAllowAllOrigins = Boolean.parseBoolean(accessControlAllowOrigin);
} catch (Exception e) { }
logger.warn("CORS access control allow ALL origins: {}", RestApiServer.accessControlAllowAllOrigins);
}
}
@Override
public void startUp(FloodlightModuleContext Context) {
// no-op
}
}